feat(sparql-anything): split a file into the chunks a conversion can hold - #799
Open
ddeboer wants to merge 2 commits into
Open
feat(sparql-anything): split a file into the chunks a conversion can hold#799ddeboer wants to merge 2 commits into
ddeboer wants to merge 2 commits into
Conversation
…hold convert() takes chunks it does not create, so every consumer was left to produce them – the shell script this package replaces does it with split(1) and a cat loop per chunk. chunk() streams a line-oriented file into fixed-row pieces and returns their paths in order, which is what a job's `chunks` takes. It belongs here rather than in a package of its own: LDE's pipeline streams and has no use for a file in pieces. Chunking exists because SPARQL Anything materialises a chunk's whole result graph before writing it, so it is part of working around this tool, and this is the package that does that. Splitting is by line, which is what the format in hand allows: a tab-separated export has no quoting mechanism, so a record cannot span lines. A delimited format that wraps a field in quotes to carry a newline would be cut in two, and the contract says so rather than parsing every field to guard against an input this cannot receive. Measured, line splitting also runs a single pass at about 6x a parser's throughput. `extension` exists for tools that read the format from the file name: SPARQL Anything does, so a .txt export of a CSV has to be chunked as .csv to be read as one. It defaults to the input's own extension, so chunking N-Triples for something that expects N-Triples needs nothing.
… on it
A review of chunk() found five things, four of them about what happens when
writing goes wrong:
- No 'error' listener was ever attached to the chunk file. once() covers only
the instants an await is pending; a write that fails while the loop waits on
the next line – ENOSPC part way through a multi-GB export – was an unhandled
'error', which ends the process rather than this call.
- The open chunk was left open on any failure, keeping its handle and half a
row, so a caller that caught and retried accumulated both.
- `extension` was concatenated unchecked, so 'csv' without its dot produced
places-0000csv, which is exactly the naming the option exists to get right.
- Chunks of the same input from an earlier call were left in place, so a
shorter re-run left a longer one's tail for a caller globbing the directory
to pick up. Removed first, files only, and only those matching this input's
own chunk names.
Closing a chunk now waits with finished() rather than once('close'): it
reports a stream that has already failed, and returns for one that has already
closed, where waiting for the event waits for one that will not come again.
That single reporting path replaced a stored error and a throw, which no test
could reach because the failure always arrived through an await first.
The fifth is documented rather than fixed: the input must hold data only, since
every line becomes a row. A file carrying its own header would repeat it inside
the first chunk, and no caller needs the alternative yet.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
convert()takes chunks it does not create, so every consumer was left to produce them – the shell script this package replaces does it withsplit(1)and acatloop per chunk.chunk()streams a line-oriented file into fixed-row pieces and returns their paths in order, which is exactly what a job'schunkstakes.Why here, and not a package of its own
I started out proposing
@lde/line-chunker, on the grounds that chunking is how an input is made to fit a bounded unit of work (ADR 12). That was wrong: in LDE, bounded memory comes from streaming, and the pipeline has no use for a file in pieces. Chunking exists because SPARQL Anything materialises a chunk's whole result graph before writing it – a property of this tool, not of the data or the pipeline. So it belongs in the package whose job is working around this tool, beside the one-JVM-per-chunk decision it is the other half of.Nothing in it touches the converter, so if a second, streaming-averse tool ever appears, promoting it is a file move.
Why by line, and not by parsing
The tempting alternative was
csv-parse, so that a quoted field containing a newline could not be cut in two. Two things against it:split -lis pure line splitting, and the parsing happens downstream in SPARQL Anything. A parser here would be a new guarantee against an input this cannot receive.csv-parsewithraw7.5 s.So the contract says what it does – splits on lines, every record must be one line – rather than parsing every field to defend against something that cannot arrive. Widening
header?: stringto also mean "the input's first line" is a later change that breaks no caller, if a headered-CSV consumer ever turns up.Measurements
Same 220 MB file, two runs each, chunks verified identical:
catcopy (raw I/O floor, macOS)split -l(Linux, Docker)split -l(macOS)csv-parsewithrawGNU
splitis faster, and shelling out to it was the other candidate. It would have meant a POSIX dependency in the runner's environment, a dev machine 12× slower than CI (BSDsplitis pathological – 3.3 s of system time to move bytes a copy handles in 0.07 s), and a package that is five lines of shell behind an interface. Against a chunking step that is seconds inside a two-minutedownload.sh, that trade is not worth it.extensionDefaults to the input's own, so chunking N-Triples for something that expects N-Triples needs nothing. It is there for tools that read the format from the file name – SPARQL Anything does, which is why the shell script renames its
.txtchunks to.csv.